﻿using System;
using System.Collections.Generic;
using System.IdentityModel.Claims;
using System.Linq;
using System.Reflection;
using System.Text;
using IdentityAtRest.Utility.Identity;
using IdentityAtRest.Utility.Identity.Authorization;
using SimpleExpressionEvaluator;
using SimpleExpressionEvaluator.Evaluation;

namespace IdentityAtRest.Service.Authorization
{
    public class ExpressionPermissionPolicy : IPermissionPolicy
    {
        private readonly string _permissionName;
        private readonly IExpression<bool> _policyExpression;

        private static readonly Dictionary<string, string> _claimAliases;
        
        static ExpressionPermissionPolicy()
        {
            _claimAliases = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
            MapAliasesFor(typeof(ClaimTypes));
            MapAliasesFor(typeof(CustomClaimTypes));
        }

        public ExpressionPermissionPolicy(string permissionName, IExpression<bool> policyExpression)
        {
            _permissionName = permissionName;
            _policyExpression = policyExpression;
        }

        public string PermissionName
        {
            get { return _permissionName; }
        }

        public IAuthorizationDecision Authorize(IList<IClaim> claims)
        {
            var context = new ExpressionContext(claims);

            context.ResolveUnknownVariable +=
                (sender, args) =>
                    {
                        if (_claimAliases.ContainsKey(args.VariableName))
                        {
                            //In an aggregate function, the current context
                            //will be a single claim, as the aggregate
                            //function will iterate through all claims in the set.
                            //Therefore, if we're in an aggregate, we need
                            //to focus on the claim at hand. Otherwise, 
                            //we'll assume the expression just wants any
                            //claim in the set, and so we'll return 
                            //the first value we find.
                            if (args.ContextObject is IClaim)
                            {
                                var claim = (IClaim)args.ContextObject;
                                if (claim.ClaimType == _claimAliases[args.VariableName])
                                    args.VariableValue = claim.Value;
                            }
                            else
                            {
                                args.VariableValue =
                                    claims.GetClaimValue<object>(_claimAliases[args.VariableName]);
                            }
                        }
                    };

            context.ResolveMissingFunction +=
                (sender, args) =>
                    {
                        if (args.FunctionName.Equals("IsInRole",StringComparison.OrdinalIgnoreCase))
                        {
                            args.Function = (funcArgs) => IsInRole(claims,funcArgs);
                        }
                    };
            
            bool result = _policyExpression.Evaluate(context);
            return new AuthorizationDecision(_permissionName, result);
        }

        private static void MapAliasesFor(Type claimType)
        {
            PropertyInfo[] props = claimType.GetProperties();
            foreach (PropertyInfo prop in props)
            {
                if (prop.PropertyType == typeof(string))
                {
                    _claimAliases[prop.Name] = (string)prop.GetValue(null, null);
                }
            }
        }

        private bool IsInRole(IList<IClaim> claims,object[] roles)
        {
            foreach (object role in roles)
            {
                string roleName = role.ToString();
                if (!claims.ClaimEqualsAny(CustomClaimTypes.Role, roleName))
                    return false;
            }
            return true;
        }
     
    }
}
